# -*- coding: utf-8 -*-
import socket
import select
import threading
import struct
from time import sleep
import os
PING = "PING"
PONG = "PONG"

class TcpClient(object):
    def __init__(self, server_ip='127.0.0.1', server_port=6000, data_handler=None, err_handler=None,
                 device_info=None, connect_status_handler=None):
        super().__init__()
        self.host = server_ip
        self.port = server_port
        self.data_hanlder = data_handler #数据处理函数
        self.err_handler = err_handler
        self.connect_status_handler = connect_status_handler
        self.err_count = 0
        self.data_buffer = bytes()
        self.socket_lock = None
        self.sock = None
        self.device_info = device_info
        print("host:%s,ip:%s",self.host, self.port)

    def _print_log(self, log):
        print('{} {}'.format(self.device_info, log))

    def _do_connect(self):
        self.sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
        try:
            self.sock.connect((self.host, self.port))
        except Exception as e:
            self._print_log('connct error, server_port:{}, errMsg:{}'.format(self.port, str(e)))
            self._print_log('当前在线adb设备:' + os.popen('adb devices').read())
            if self.err_handler:
                self.err_handler('connect failed')
            raise

    def connect(self):
        self._do_connect()

        # 创建线程锁，防止主线程socket被close了，子线程还在recv而引发的异常
        self.socket_lock = threading.Lock()

        def read_thread_method():
            while True: 
                if not self.sock:  # 如果socket关闭，退出
                    break
                # 使用select监听客户端（这里客户端需要不停接收服务端的数据，所以监听客户端）
                # 第一个参数是要监听读事件列表，因为是客户端，我们只监听创建的一个socket就ok
                # 第二个参数是要监听写事件列表，
                # 第三个参数是要监听异常事件列表，
                # 最后一个参数是监听超时时间，默认永不超时。如果设置了超时时间，过了超时时间线程就不会阻塞在select方法上，会继续向下执行
                # 返回参数 分别对应监听到的读事件列表，写事件列表，异常事件列表
                rs, _, _ = select.select([self.sock], [], [], 10)
                for r in rs:  # 我们这里只监听读事件，所以只管读的返回句柄数组
                    self.socket_lock.acquire()  # 在读取之前先加锁，锁定socket对象（sock是主线程和子线程的共享资源，锁定了sock就能保证子线程在使用sock时，主线程无法对sock进行操作）

                    if not self.sock:  # 这里需要判断下，因为有可能在select后到加锁之间socket被关闭了
                        self.socket_lock.release()
                        break

                    data = r.recv(1024)  # 读数据，按自己的方式读

                    self.socket_lock.release()  # 读取完成之后解锁，释放资源

                    if not data:
                        self._print_log('server close, server_port:{}'.format(self.port))
                        self.connect_status_handler(True)
                        if self.err_count > 15:
                            self.disconnect()
                            if self.err_handler:
                                self.err_handler('socket disconnected, server_port:{}'.format(self.port))
                            break
                        
                        # 重连
                        sleep(2)
                        self._print_log('do reconnect, server_port:{}'.format(self.port))
                        self._do_connect()
                        self.err_count += 1

                    else:
                        self.connect_status_handler(False)
                        self.err_count = 0
                        self._data_received(data)

        # 创建一个线程去读取数据
        read_thread = threading.Thread(target=read_thread_method)
        read_thread.setDaemon(True)
        read_thread.start()
    
    def _data_received(self, data):
        self.data_buffer += data
        headerSize = 4

        while True:
            if len(self.data_buffer) < headerSize:
                return

            # 读取消息头部
            # struct中:!代表Network order，I代表1个unsigned int数据
            headPack = struct.unpack('!I', self.data_buffer[:headerSize])
            # 获取消息正文长度
            bodySize = headPack[0]

            # 分包情况处理
            if len(self.data_buffer) < headerSize+bodySize :
                return

            # 读取消息正文的内容
            body = self.data_buffer[headerSize:headerSize+bodySize]
            try:
                # 处理数据
                self._process_data(str(body, encoding='utf-8'))
            finally:
                # 粘包情况的处理
                self.data_buffer = self.data_buffer[headerSize+bodySize:]


    def disconnect(self):
        # 清理socket，同样道理，这里需要锁定和解锁
        if self.socket_lock:
            self.socket_lock.acquire()
        if self.sock:
            self.sock.close()
            self.sock = None
        if self.socket_lock:
            self.socket_lock.release()
        self._print_log("disconnect server:{}, port:{}".format(self.host, self.port))


    def send_msg(self, msg):
        # header = [len(msg)]
        # 字符串包含中文的情况
        header = [len(msg.encode('utf-8'))]

        headPack = struct.pack("!I", *header)

        body = bytes(msg, encoding = "utf8")
        sendData = headPack+body
        # 处理网络异常
        try:
            self.sock.send(sendData)
        except:
            self._print_log(" send msg error: socket is disconnected")
            self._print_log('当前在线adb设备:' + os.popen('adb devices').read())
            if self.err_handler:
                self.err_handler("socket is disconnected")
            # raise   # ys:2020/9/15 不抛异常，避免意外阻塞pytest进程


    def _process_data(self, data):
        if data == PING:
            self.send_msg(PONG)
        elif self.data_hanlder:
            self.data_hanlder(data)


if __name__ == "__main__":

    def test_data_handler(data):
        print("<<<<<<  {}".format(data))


    tcp_client = TcpClient(data_handler=test_data_handler)

    tcp_client.connect()
    
#     cmd = """
#       [{field:yy.com.thunderbolt.config.DemoConfig, param_name:[mUid, mChannelId], param_type:[String, String], param_value:[234,567]},
#  {api:joinRoom, param_name:[token, roomName, uid], param_type:["byte[]", String, String], param_value:[123,1234,12344]},
#  {api:joinRoom, param_name:[token, roomName, uid], param_type:["byte[]", String, String], param_value:[123,1234,12344]},
#  {api:yy.com.thunderbolt.manager.MediaManager.clearCallback}
# ]
#     """

#     tcpClient.sendMsg(cmd)


    def bvt_test_CreDes_1():
        cmd = """
        [
            {field:yy.com.thunderbolt.config.DemoConfig, param_name:[mUid, mChannelId], param_type:[String, String], param_value:[234,567]},
            {api:yy.com.thunderbolt.manager.MediaManager.initialize},
            {api:yy.com.thunderbolt.manager.MediaManager.deInitialize}
        ]
        """
        tcp_client.send_msg(cmd)


    cmd = "[\"{'api': 'setLocalVideoCanvas', 'param_name': ['local'], 'param_type': ['ThunderVideoCanvas'], 'param_value': [['9217118']]}\", \"{'api': 'setLocalCanvasScaleMode', 'param_name': ['mode'], 'param_type': ['int'], 'param_value': [1]}\"]"
    tcp_client.send_msg(cmd)

    for i in range(1000):
        print("---{}".format(i))
        # bvt_test_CreDes_1()
        tcp_client.send_msg(cmd)
        sleep(1)

    
    # [{funName1:[{type:value}]},{funName2:[{type:value}]}]

    # 执行函数和参数， & 顺序
    # 参数： 类型和顺序
#         [
#  {"api":"joinRoom"}, 
#  {"api":"addSubscribe", param_name:[room, uid], param_type:[String, String"], param_value:["134", "2345"]}, 
#  {"api":"setVideoEncoderConfig", param_name:["config"], param_type:["ThunderVideoEncoderConfiguration"]， param_value:[["1", "2"]]}
# ]

    # public int setEqGains(final int[] gains)
    # public int sendUserAppMsgData(byte[] msgData)
    #  public int sendMediaExtraInfo(java.nio.ByteBuffer data, int dataLen)
    # public int setVideoEncoderConfig(ThunderVideoEncoderConfiguration yyVideoConfig)
    #  private String getPrintString(Object obj)


    sleep(120)
    tcp_client.disconnect()


